Skip to content

feat(policies): PolicyDescriptor + nested test body + XACML PERMIT#16

Merged
jlc488 merged 1 commit into
mainfrom
feat/policies-contract-gap
May 27, 2026
Merged

feat(policies): PolicyDescriptor + nested test body + XACML PERMIT#16
jlc488 merged 1 commit into
mainfrom
feat/policies-contract-gap

Conversation

@jlc488

@jlc488 jlc488 commented May 27, 2026

Copy link
Copy Markdown
Contributor

Third PR in the admin-ui ↔ backend contract-gap series (after #14 menus, #15 audit-logs). The Policies page needs name+description rows for the list and a nested (subject, action, resource, environment) body for the dry-run tester; this controller returned bare strings and accepted a flat request with name in the path.

Summary

SPI changes

  • PolicyDecision: ALLOWPERMIT. Adopt the XACML 3.0 vocabulary (PERMIT / DENY / NOT_APPLICABLE) so the admin UI's Tag rendering and downstream auditors see the standard names.
  • Policy interface: new default String description() { return null; } hook. Backward-compatible — existing policies don't need to change unless they want to surface human-readable text in the UI.
  • DefaultPolicyEvaluator: new registeredPolicies() returning the Policy objects (not just names) so the admin controller can read descriptions without changing the PolicyEvaluator contract.

Test

  • DefaultPolicyEvaluatorTest — renamed returnsAllowFromMatchingPolicyreturnsPermitFromMatchingPolicy and updated all PolicyDecision references.

Controller rewrite

  • GET /admin/api/v1/policies[{name, description}] sorted by name. Matches the admin UI's PolicyDescriptor interface.
  • POST /admin/api/v1/policies/test (no name in the path) takes the admin UI's nested body:
    {
      "policyName": "...",
      "subject":  { "userId": "...", "tenantId": "...", "attributes": { ... } },
      "action":   "...",
      "resource": { "type": "...", "id": "...", "attributes": { ... } },
      "environment": { ... }
    }
  • Response: {effect, reason, matchedRules} matching the UI's PolicyTestResponse.

Honest caveats (queued follow-ups, not in this PR)

  1. Subject attributes and action aren't consumed by the current PolicyContext — it has resourceAttributes and environmentAttributes but no subject-attribute slot. Subject attributes flow through environment for now; promoting them to first-class lives in a follow-up.
  2. reason and matchedRules are placeholders today because Policy.evaluate still returns only a PolicyDecision enum with no detail. Promoting the SPI to return a richer decision (PolicyEvaluation { decision, reason, matchedRules }) is queued as a follow-up so this PR stays focused on the wire contract.

Test plan

  • ./gradlew build green (55 tasks, all tests pass — only the access-core test needed updating for ALLOW→PERMIT)
  • Wire the admin-ui Policies page against this; verify the list renders descriptions and the test dialog roundtrips a PERMIT policy.

What's next in this series

Final closures: Tenants (status enum + createdAt + PUT /{id}/status), Diagnostics + Settings field rename.

Closes the third contract gap with the admin UI. The Policies page
needs name+description rows for the list and a nested
(subject, action, resource, environment) body for the dry-run tester;
this controller returned bare strings and accepted a flat request
with `name` in the path.

SPI
---
- PolicyDecision: ALLOW -> PERMIT. Adopt the XACML 3.0 vocabulary
  (PERMIT / DENY / NOT_APPLICABLE) so the admin UI's Tag rendering
  and downstream auditors see the standard names.
- Policy interface: new `default String description() { return null; }`
  hook. Backward-compatible — existing policies don't need to change
  unless they want to surface human-readable text in the admin UI.
- DefaultPolicyEvaluator: new `registeredPolicies()` returning the
  Policy objects (not just names) so the admin controller can read
  descriptions without changing the PolicyEvaluator contract.

Test
----
- DefaultPolicyEvaluatorTest renamed `returnsAllowFromMatchingPolicy`
  -> `returnsPermitFromMatchingPolicy` and updated all PolicyDecision
  references.

Controller rewrite
------------------
- `GET /admin/api/v1/policies` returns
  `[{name, description}]` sorted by name — matches the admin UI's
  PolicyDescriptor interface.
- `POST /admin/api/v1/policies/test` (no `name` in the path) takes
  the admin UI's nested body:
    {
      policyName,
      subject: {userId, tenantId?, attributes?},
      action,
      resource: {type, id?, attributes?},
      environment
    }
  The subject `userId` / `tenantId` and the resource fields feed
  PolicyContext.builder; subject attributes and action aren't
  consumed by the current PolicyContext (subject attributes flow
  through environment for now) — that's queued as a follow-up.
- Response: `{effect, reason, matchedRules}` matching the UI's
  PolicyTestResponse. `reason` / `matchedRules` are placeholders
  today because Policy still returns only a PolicyDecision enum
  with no detail; promoting the SPI to return a richer decision
  is queued as a follow-up so this PR stays focused on the wire
  contract.
@jlc488
jlc488 merged commit 208ace1 into main May 27, 2026
1 check passed
@jlc488
jlc488 deleted the feat/policies-contract-gap branch May 30, 2026 06:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant